[Rust] rustlsとOSの証明書機能を使ったTLS接続
Introduction
rustlsはRustで書かれたTLSライブラリです。
OpenSSLおよびBoringSSLのパフォーマンスを超えたとも言われています。
また、rustls-platform-verifierは、TLS証明書の検証をOSの証明書機能に基づいて証明書を検証するライブラリです。
※Macであればキーチェーンを使う
今回はこれらを使ってクライアントとサーバプログラムを作成し、TLS通信してみます。
Environment
- MacBook Pro (14-inch, M3, 2023)
- OS : MacOS 14.5
- Rust : 1.81.0
Try
ではCargoでプロジェクトを作成し、serverプログラムとclientプログラムでTLS通信してみます。
まずはCargoでプロジェクトを作成します。
% cargo new rusttls-example && cd rusttls-example
Cargo.toml
を下記のように記述。
[dependencies]
env_logger = "0.11.5"
hex = "0.4.3"
log = "0.4.22"
rustls = "0.23.15"
rustls-pemfile = "2.2.0"
rustls-platform-verifier = "0.3.4"
tokio = { version = "1", features = ["full"] }
tokio-rustls = "0.26.0"
[[example]]
name = "tls-server"
path = "src/tls-server.rs"
[[example]]
name = "tls-client"
path = "src/tls-client.rs"
ターミナルで自己署名証明書の生成します。
とりあえずテスト用なので適当に作成します。
% openssl req -x509 -newkey rsa:4096 -nodes -keyout server.key -out server.crt -days 365 -subj "/CN=localhost"
Server
TLSサーバ側の主な処理です。
証明書(公開鍵を含む)と秘密鍵をロードします。
let cert_file = std::fs::read("server.crt")?;
let key_file = std::fs::read("server.key")?;
// 証明書の読み込み
let certs = rustls_pemfile::certs(&mut &*cert_file)
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.map(CertificateDer::from)
.collect();
TLSサーバーの設定。
let mut config = ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(certs, key)?;
// ALPNプロトコルの設定(今回はHTTPを使わないのでなくてもOK)
config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
あとはリスナーを作成します。
8443番ポートで待ち受けてサーバ側の処理はOKです。
↓がtls-server.rs全文です。
use rustls::{
pki_types::{CertificateDer, PrivateKeyDer},
ServerConfig,
};
use std::sync::Arc;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
use rustls::crypto::ring::default_provider;
async fn run_tls_server() -> Result<(), Box<dyn std::error::Error>> {
// CryptoProvider
default_provider().install_default();
let cert_file = std::fs::read("server.crt")?;
let key_file = std::fs::read("server.key")?;
println!("Loading certificate and key...");
let certs = rustls_pemfile::certs(&mut &*cert_file)
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.map(CertificateDer::from)
.collect();
println!("Certificate loaded successfully");
let key = {
let mut reader = &mut &*key_file;
let mut private_keys = Vec::new();
for item in rustls_pemfile::read_all(&mut reader) {
match item {
Ok(rustls_pemfile::Item::Pkcs1Key(key)) => {
println!("Found PKCS1 key");
private_keys.push(PrivateKeyDer::Pkcs1(key));
}
Ok(rustls_pemfile::Item::Pkcs8Key(key)) => {
println!("Found PKCS8 key");
private_keys.push(PrivateKeyDer::Pkcs8(key));
}
Ok(_) => println!("Found other item"),
Err(e) => println!("Error reading key: {}", e),
}
}
private_keys
.into_iter()
.next()
.ok_or("no private key found")?
};
println!("Private key loaded successfully");
let mut config = ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(certs, key)?;
config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
println!("Server configuration created successfully");
let acceptor = tokio_rustls::TlsAcceptor::from(Arc::new(config));
let listener = TcpListener::bind("127.0.0.1:8443").await?;
println!("TLS Server listening on port 8443");
while let Ok((stream, addr)) = listener.accept().await {
println!("Accepted connection from: {}", addr);
let acceptor = acceptor.clone();
tokio::spawn(async move {
match acceptor.accept(stream).await {
Ok(mut tls_stream) => {
println!("TLS connection established with: {}", addr);
let mut buf = [0; 1024];
match tls_stream.read(&mut buf).await {
Ok(n) => {
println!("\n=== TLS Server Received ===");
println!("Decrypted text: {}", String::from_utf8_lossy(&buf[..n]));
println!("Raw bytes: {}", hex::encode(&buf[..n]));
if let Err(e) = tls_stream.write_all(&buf[..n]).await {
eprintln!("Failed to write to TLS socket: {}", e);
}
}
Err(e) => eprintln!("Failed to read from TLS socket: {}", e),
}
}
Err(e) => eprintln!("TLS acceptance failed: {}", e),
}
});
}
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
run_tls_server().await
}
サーバを起動し、クライアントから正しく接続できれば
復号したテキストが表示されます。
Client
次はクライアント側のプログラムです。
まずはTLSクライアントの設定をします。
// プラットフォーム(macなのでkey chain)の証明書検証を設定
let _verifier = Verifier::new();
let mut config = rustls_platform_verifier::tls_config();
// ALPNプロトコルの設定(なくてもOK)
config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
そしてTLSサーバへ接続とデータ送受信を実施します。
let connector = tokio_rustls::TlsConnector::from(Arc::new(config));
let server_name = rustls::pki_types::ServerName::try_from("localhost")?
.to_owned();
// TCP接続とTLSハンドシェイク
let stream = TcpStream::connect("127.0.0.1:8443").await?;
let mut tls_stream = connector.connect(server_name, stream).await?;
// データ送信
tls_stream.write_all(message).await?;
// データ受信
let mut buf = [0; 1024];
let n = tls_stream.read(&mut buf).await?;
クライアント側プログラムの全文です。
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use rustls::crypto::ring::default_provider;
use std::sync::Arc;
use rustls_platform_verifier::Verifier;
use std::env;
async fn run_tls_client() -> Result<(), Box<dyn std::error::Error>> {
env::set_var("RUST_LOG", "debug,rustls=debug");
env_logger::init();
// CryptoProviderをインストール
if let Err(e) = default_provider().install_default() {
eprintln!("Failed to install default provider: {:?}", e);
}
println!("Creating TLS client configuration...");
// プラットフォームの証明書検証を設定
let _verifier = Verifier::new();
println!("Platform verifier created");
// TLS設定を作成
let mut config = rustls_platform_verifier::tls_config();
// ALPNプロトコルの設定(なくてもOK)
config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
println!("Connecting to server...");
// TLS接続の準備
let connector = tokio_rustls::TlsConnector::from(Arc::new(config));
let server_name = rustls::pki_types::ServerName::try_from("localhost")?
.to_owned();
// TCP接続
let stream = TcpStream::connect("127.0.0.1:8443").await?;
println!("TCP connection established");
// TLSハンドシェイク
let mut tls_stream = connector.connect(server_name, stream).await
.map_err(|e| {
eprintln!("TLS connection failed: {}", e);
e
})?;
println!("TLS connection established");
// テストメッセージの送信
let message = b"Hello, World!";
println!("\n=== TLS Client Sending ===");
println!("Sending text: {}", String::from_utf8_lossy(message));
println!("Raw bytes: {}", hex::encode(message));
tls_stream.write_all(message).await?;
// サーバーからの応答を受信
let mut buf = [0; 1024];
let n = tls_stream.read(&mut buf).await?;
println!("\n=== TLS Client Received ===");
println!("Decrypted text: {}", String::from_utf8_lossy(&buf[..n]));
println!("Raw bytes: {}", hex::encode(&buf[..n]));
// TLS接続を正しく終了
tls_stream.shutdown().await?;
println!("Connection closed successfully");
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
run_tls_client().await
}
動作確認
では動かしてみます。
このプログラム(tls-server.rsとtls-client.rs)を実行すると、
OSの証明書ストア(今回はキーチェーン)を使用して証明書の検証が行われ、
TLS通信が確立され、暗号化されたデータの送受信が行われます。
まずはTLSサーバを起動。
% cargo run --example tls-server
・・・
Loading certificate and key...
Certificate loaded successfully
Found PKCS8 key
Private key loaded successfully
Server configuration created successfully
TLS Server listening on port 8443
クライアントを実行してみます。
最初に証明書をインストールせずに実行してみます。
証明書がないので通信に失敗します。
% cargo run --example tls-client
Creating TLS client configuration...
Platform verifier created
Connecting to server...
TCP connection established
・・・
[2024-11-06T01:39:25Z ERROR rustls_platform_verifier::verification::apple] failed to verify TLS certificate: invalid peer certificate: Other(OtherError("“localhost” certificate is not trusted: -67843"))
TLS connection failed: invalid peer certificate: Other(OtherError("“localhost” certificate is not trusted: -67843"))
Error: Custom { kind: InvalidData, error: InvalidCertificate(Other(OtherError("“localhost” certificate is not trusted: -67843"))) }
証明書をkey chainに追加してから再度実行してみます。
% sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain server.crt
キーチェーンから証明書を検索してちゃんとインストールされているか確認してみます。
下記のように表示されれば、証明書が信頼された状態となりTLS接続が成功します。
※テスト用です
% security find-certificate -a -c "localhost"
keychain: "/Library/Keychains/System.keychain"
version: 256
class: 0x80001000
attributes:
"alis"<blob>="localhost"
"cenc"<uint32>=0x00000003
"ctyp"<uint32>=0x00000001
"hpky"<blob>=0x8FDEE7AAE1F2FD9AE5BEF46DFF2D95546A17ED37 "\217\336\347\252\341\362\375\232\345\276\364m\377-\225Tj\027\3557"
"issu"<blob>=0x30143112301006035504030C096C6F63616C686F7374 "0\0241\0220\020\006\003U\004\003\014\011localhost"
"labl"<blob>="localhost"
"skid"<blob>=0x8FDEE7AAE1F2FD9AE5BEF46DFF2D95546A17ED37 "\217\336\347\252\341\362\375\232\345\276\364m\377-\225Tj\027\3557"
"snbr"<blob>=0x1796BADAB78E8B5A8FE92786680B901327D109A5 "\027\226\272\332\267\216\213Z\217\351'\206h\013\220\023'\321\011\245"
"subj"<blob>=0x30143112301006035504030C096C6F63616C686F7374 "0\0241\0220\020\006\003U\004\003\014\011localhost"
今度はクライアント接続も成功しました。
% cargo run --example tls-client
Creating TLS client configuration...
Platform verifier created
Connecting to server...
TCP connection established
[2024-11-06T01:41:10Z DEBUG rustls::client::hs] No cached session for DnsName("localhost")
[2024-11-06T01:41:10Z DEBUG rustls::client::hs] Not resuming any session
[2024-11-06T01:41:10Z DEBUG rustls::client::hs] Using ciphersuite TLS13_AES_256_GCM_SHA384
[2024-11-06T01:41:10Z DEBUG rustls::client::tls13] Not resuming
[2024-11-06T01:41:10Z DEBUG rustls::client::tls13] TLS1.3 encrypted extensions: [Protocols([ProtocolName(6832)]), ServerNameAck]
[2024-11-06T01:41:10Z DEBUG rustls::client::hs] ALPN protocol is Some(b"h2")
TLS connection established
=== TLS Client Sending ===
Sending text: Hello, World!
Raw bytes: 48656c6c6f2c20576f726c6421
=== TLS Client Received ===
Decrypted text: Hello, World!
Raw bytes: 48656c6c6f2c20576f726c6421
[2024-11-06T01:41:10Z DEBUG rustls::common_state] Sending warning alert CloseNotify
Connection closed successfully
サーバ側にもログがでてます。
=== TLS Server Received ===
Decrypted text: Hello, World!
Raw bytes: 48656c6c6f2c20576f726c6421
Summary
今回はrusttlsを使ってクライアント-サーバ間でTLS接続を実装してみました。
なんか昔Delphiとcgiのクラサバでこんな感じで証明書インストールして使ってた気がします。